<?php

/* SVN FILE: $Id$ */
/**
 * Short description for file.
 *
 * Long description for file
 *
 * PHP versions 4 and 5
 *
 * CakePHP(tm) :  Rapid Development Framework (http://www.cakephp.org)
 * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 *
 * @copyright     Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
 *
 * @link          http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
 * @since         CakePHP(tm) v 0.10.0.1076
 *
 * @version       $Revision: 8120 $
 * @modifiedby    $LastChangedBy: gwoo $
 * @lastmodified  $Date: 2009-03-19 13:25:10 -0700 (Thu, 19 Mar 2009) $
 *
 * @license       http://www.opensource.org/licenses/mit-license.php The MIT License
 */
App::import('Core', array('Set', 'String'));

/**
 * DboSource.
 *
 * Creates DBO-descendant objects from a given db connection configuration
 */
class DboSource extends DataSource
{
    /*
 * Description string for this Database Data Source.
 *
 * @var unknown_type
 */
    public $description = 'Database Data Source';
/*
 * index definition, standard cake, primary, index, unique
 *
 * @var array
 */
    public $index = array('PRI' => 'primary', 'MUL' => 'index', 'UNI' => 'unique');
/*
 * Database keyword used to assign aliases to identifiers.
 *
 * @var string
 */
    public $alias = 'AS ';
/*
 * Caches fields quoted in DboSource::name()
 *
 * @var array
 */
    public $fieldCache = array();
/*
 * Bypass automatic adding of joined fields/associations.
 *
 * @var boolean
 */
    public $__bypass = false;
/*
 * The set of valid SQL operations usable in a WHERE statement
 *
 * @var array
 */
    public $__sqlOps = array('like', 'ilike', 'or', 'not', 'in', 'between', 'regexp', 'similar to');
/*
 * Index of basic SQL commands
 *
 * @var array
 * @access protected
 */
    public $_commands = array(
        'begin' => 'BEGIN',
        'commit' => 'COMMIT',
        'rollback' => 'ROLLBACK',
    );
    /**
     * Constructor.
     */
    public function __construct($config = null, $autoConnect = true)
    {
        if (!isset($config['prefix'])) {
            $config['prefix'] = '';
        }
        parent::__construct($config);
        //$this->fullDebug = Configure::read() > 1;
        $this->fullDebug = true;
        if ($autoConnect) {
            return $this->connect();
        } else {
            return true;
        }
    }
    /**
     * Reconnects to database server with optional new settings.
     *
     * @param array $config An array defining the new configuration settings
     *
     * @return bool True on success, false on failure
     */
    public function reconnect($config = null)
    {
        $this->disconnect();
        $this->setConfig($config);
        $this->_sources = null;

        return $this->connect();
    }
    /**
     * Prepares a value, or an array of values for database queries by quoting and escaping them.
     *
     * @param mixed  $data   A value or an array of values to prepare.
     * @param string $column The column into which this data will be inserted
     * @param bool   $read   Value to be used in READ or WRITE context
     *
     * @return mixed Prepared value or array of values.
     */
    public function value($data, $column = null, $read = true)
    {
        if (is_array($data) && !empty($data)) {
            return array_map(
                array(&$this, 'value'),
                $data, array_fill(0, count($data), $column), array_fill(0, count($data), $read)
            );
        } elseif (is_object($data) && isset($data->type)) {
            if ($data->type == 'identifier') {
                return $this->name($data->value);
            } elseif ($data->type == 'expression') {
                return $data->value;
            }
        } elseif (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) {
            return $data;
        } else {
            return;
        }
    }
    /**
     * Returns an object to represent a database identifier in a query.
     *
     * @param string $identifier
     *
     * @return object An object representing a database identifier to be used in a query
     */
    public function identifier($identifier)
    {
        $obj = new stdClass();
        $obj->type = 'identifier';
        $obj->value = $identifier;

        return $obj;
    }
    /**
     * Returns an object to represent a database expression in a query.
     *
     * @param string $expression
     *
     * @return object An object representing a database expression to be used in a query
     */
    public function expression($expression)
    {
        $obj = new stdClass();
        $obj->type = 'expression';
        $obj->value = $expression;

        return $obj;
    }
    /**
     * Executes given SQL statement.
     *
     * @param string $sql SQL statement
     *
     * @return unknown
     */
    public function rawQuery($sql)
    {
        $this->took = $this->error = $this->numRows = false;

        return $this->execute($sql);
    }

    /**
     * Queries the database with given SQL statement, and obtains some metadata about the result
     * (rows affected, timing, any errors, number of rows in resultset). The query is also logged.
     * If DEBUG is set, the log is shown all the time, else it is only shown on errors.
     *
     * @param string $sql
     * @param array  $options
     *
     * @return mixed Resource or object representing the result set, or false on failure
     */
    public function execute($sql, $options = array())
    {
        $defaults = array('stats' => true, 'log' => $this->fullDebug);
        $options = array_merge($defaults, $options);

        if ($options['stats']) {
            $t = getMicrotime();
            $this->_result = $this->_execute($sql);
            $this->took = round((getMicrotime() - $t) * 1000, 0);
            $this->affected = $this->lastAffected();
            $this->error = $this->lastError();
            $this->numRows = $this->lastNumRows();
        }
        if ($options['log']) {
            $this->logQuery($sql);
        }

        if ($this->error) {
            $this->showQuery($sql);

            return false;
        } else {
            return $this->_result;
        }
    }
    /**
     * DataSource Query abstraction.
     *
     * @return resource Result resource identifier
     */
    public function query()
    {
        $args = func_get_args();
        $fields = null;
        $order = null;
        $limit = null;
        $page = null;
        $recursive = null;

        if (count($args) == 1) {
            return $this->fetchAll($args[0]);
        } elseif (count($args) > 1 && (strpos(strtolower($args[0]), 'findby') === 0 || strpos(strtolower($args[0]), 'findallby') === 0)) {
            $params = $args[1];

            if (strpos(strtolower($args[0]), 'findby') === 0) {
                $all = false;
                $field = Inflector::underscore(preg_replace('/^findBy/i', '', $args[0]));
            } else {
                $all = true;
                $field = Inflector::underscore(preg_replace('/^findAllBy/i', '', $args[0]));
            }

            $or = (strpos($field, '_or_') !== false);
            if ($or) {
                $field = explode('_or_', $field);
            } else {
                $field = explode('_and_', $field);
            }
            $off = count($field) - 1;

            if (isset($params[1 + $off])) {
                $fields = $params[1 + $off];
            }

            if (isset($params[2 + $off])) {
                $order = $params[2 + $off];
            }

            if (!array_key_exists(0, $params)) {
                return false;
            }

            $c = 0;
            $conditions = array();

            foreach ($field as $f) {
                $conditions[$args[2]->alias.'.'.$f] = $params[$c];
                ++$c;
            }

            if ($or) {
                $conditions = array('OR' => $conditions);
            }

            if ($all) {
                if (isset($params[3 + $off])) {
                    $limit = $params[3 + $off];
                }

                if (isset($params[4 + $off])) {
                    $page = $params[4 + $off];
                }

                if (isset($params[5 + $off])) {
                    $recursive = $params[5 + $off];
                }

                return $args[2]->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
            } else {
                if (isset($params[3 + $off])) {
                    $recursive = $params[3 + $off];
                }

                return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive'));
            }
        } else {
            if (isset($args[1]) && $args[1] === true) {
                return $this->fetchAll($args[0], true);
            } elseif (isset($args[1]) && !is_array($args[1])) {
                return $this->fetchAll($args[0], false);
            } elseif (isset($args[1]) && is_array($args[1])) {
                $offset = 0;
                if (isset($args[2])) {
                    $cache = $args[2];
                } else {
                    $cache = true;
                }
                $args[1] = array_map(array(&$this, 'value'), $args[1]);

                return $this->fetchAll(String::insert($args[0], $args[1]), $cache);
            }
        }
    }
    /**
     * Returns a row from current resultset as an array.
     *
     * @return array The fetched row as an array
     */
    public function fetchRow($sql = null)
    {
        if (!empty($sql) && is_string($sql) && strlen($sql) > 5) {
            if (!$this->execute($sql)) {
                return;
            }
        }

        if ($this->hasResult()) {
            $this->resultSet($this->_result);
            $resultRow = $this->fetchResult();

            return $resultRow;
        } else {
            return;
        }
    }
    /**
     * Returns an array of all result rows for a given SQL query.
     * Returns false if no rows matched.
     *
     * @param string $sql   SQL statement
     * @param bool   $cache Enables returning/storing cached query results
     *
     * @return array Array of resultset rows, or false if no rows matched
     */
    public function fetchAll($sql, $cache = true, $modelName = null)
    {
        if ($cache && isset($this->_queryCache[$sql])) {
            if (preg_match('/^\s*select/i', $sql)) {
                return $this->_queryCache[$sql];
            }
        }

        if ($this->execute($sql)) {
            $out = array();

            $first = $this->fetchRow();
            if ($first != null) {
                $out[] = $first;
            }
            while ($this->hasResult() && $item = $this->fetchResult()) {
                $out[] = $item;
            }

            if ($cache) {
                if (strpos(trim(strtolower($sql)), 'select') !== false) {
                    $this->_queryCache[$sql] = $out;
                }
            }

            return $out;
        } else {
            return false;
        }
    }
    /**
     * Returns a single field of the first of query results for a given SQL query, or false if empty.
     *
     * @param string $name Name of the field
     * @param string $sql  SQL query
     *
     * @return unknown
     */
    public function field($name, $sql)
    {
        $data = $this->fetchRow($sql);

        if (!isset($data[$name]) || empty($data[$name])) {
            return false;
        } else {
            return $data[$name];
        }
    }
    /**
     * Returns a quoted name of $data for use in an SQL statement.
     * Strips fields out of SQL functions before quoting.
     *
     * @param string $data
     *
     * @return string SQL field
     */
    public function name($data)
    {
        if ($data == '*') {
            return '*';
        }
        if (is_object($data) && isset($data->type)) {
            return $data->value;
        }
        $array = is_array($data);
        $data = (array) $data;
        $count = count($data);

        for ($i = 0; $i < $count; ++$i) {
            if ($data[$i] == '*') {
                continue;
            }
            if (strpos($data[$i], '(') !== false && preg_match_all('/([^(]*)\((.*)\)(.*)/', $data[$i], $fields)) {
                $fields = Set::extract($fields, '{n}.0');

                if (!empty($fields[1])) {
                    if (!empty($fields[2])) {
                        $data[$i] = $fields[1].'('.$this->name($fields[2]).')'.$fields[3];
                    } else {
                        $data[$i] = $fields[1].'()'.$fields[3];
                    }
                }
            }
            $data[$i] = str_replace('.', $this->endQuote.'.'.$this->startQuote, $data[$i]);
            $data[$i] = $this->startQuote.$data[$i].$this->endQuote;
            $data[$i] = str_replace($this->startQuote.$this->startQuote, $this->startQuote, $data[$i]);
            $data[$i] = str_replace($this->startQuote.'(', '(', $data[$i]);
            $data[$i] = str_replace(')'.$this->startQuote, ')', $data[$i]);

            if (preg_match('/\s+AS\s+/', $data[$i])) {
                if (preg_match('/\w+\s+AS\s+/', $data[$i])) {
                    $quoted = $this->endQuote.' AS '.$this->startQuote;
                    $data[$i] = str_replace(' AS ', $quoted, $data[$i]);
                } else {
                    $quoted = ' AS '.$this->startQuote;
                    $data[$i] = str_replace(' AS ', $quoted, $data[$i]).$this->endQuote;
                }
            }

            if (!empty($this->endQuote) && $this->endQuote == $this->startQuote) {
                if (substr_count($data[$i], $this->endQuote) % 2 == 1) {
                    if (substr($data[$i], -2) == $this->endQuote.$this->endQuote) {
                        $data[$i] = substr($data[$i], 0, -1);
                    } else {
                        $data[$i] = trim($data[$i], $this->endQuote);
                    }
                }
            }
            if (strpos($data[$i], '*')) {
                $data[$i] = str_replace($this->endQuote.'*'.$this->endQuote, '*', $data[$i]);
            }
            $data[$i] = str_replace($this->endQuote.$this->endQuote, $this->endQuote, $data[$i]);
        }

        return (!$array) ? $data[0] : $data;
    }
    /**
     * Checks if it's connected to the database.
     *
     * @return bool True if the database is connected, else false
     */
    public function isConnected()
    {
        return $this->connected;
    }
    /**
     * Checks if the result is valid.
     *
     * @return bool True if the result is valid else false
     */
    public function hasResult()
    {
        return is_resource($this->_result);
    }
    /**
     * Outputs the contents of the queries log.
     *
     * @param bool $sorted
     */
    public function showLog($sorted = false)
    {
        if ($sorted) {
            $log = sortByKey($this->_queriesLog, 'took', 'desc', SORT_NUMERIC);
        } else {
            $log = $this->_queriesLog;
        }

        if ($this->_queriesCnt > 1) {
            $text = 'queries';
        } else {
            $text = 'query';
        }

        if (PHP_SAPI != 'cli') {
            print('<table class="cake-sql-log" id="cakeSqlLog_'.preg_replace('/[^A-Za-z0-9_]/', '_', uniqid(time(), true))."\" summary=\"Cake SQL Log\" cellspacing=\"0\" border = \"0\">\n<caption>({$this->configKeyName}) {$this->_queriesCnt} {$text} took {$this->_queriesTime} ms</caption>\n");
            print("<thead>\n<tr><th>Nr</th><th>Query</th><th>Error</th><th>Affected</th><th>Num. rows</th><th>Took (ms)</th></tr>\n</thead>\n<tbody>\n");

            foreach ($log as $k => $i) {
                print('<tr><td>'.($k + 1).'</td><td>'.h($i['query'])."</td><td>{$i['error']}</td><td style = \"text-align: right\">{$i['affected']}</td><td style = \"text-align: right\">{$i['numRows']}</td><td style = \"text-align: right\">{$i['took']}</td></tr>\n");
            }
            print("</tbody></table>\n");
        } else {
            foreach ($log as $k => $i) {
                print(($k + 1).". {$i['query']} {$i['error']}\n");
            }
        }
    }
    /**
     * Log given SQL query.
     *
     * @param string $sql SQL statement
     * @todo: Add hook to log errors instead of returning false
     */
    public function logQuery($sql)
    {
        ++$this->_queriesCnt;
        $this->_queriesTime += $this->took;
        $this->_queriesLog[] = array(
            'query' => $sql,
            'error' => $this->error,
            'affected' => $this->affected,
            'numRows' => $this->numRows,
            'took' => $this->took,
        );
        if (count($this->_queriesLog) > $this->_queriesLogMax) {
            array_pop($this->_queriesLog);
        }
        if ($this->error) {
            return false;
        }
    }
    /**
     * Output information about an SQL query. The SQL statement, number of rows in resultset,
     * and execution time in microseconds. If the query fails, an error is output instead.
     *
     * @param string $sql Query to show information on.
     */
    public function showQuery($sql)
    {
        $error = $this->error;
        if (strlen($sql) > 200 && !$this->fullDebug && Configure::read() > 1) {
            $sql = substr($sql, 0, 200).'[...]';
        }
        if (Configure::read() > 0) {
            $out = null;
            if ($error) {
                trigger_error("<span style = \"color:Red;text-align:left\"><b>SQL Error:</b> {$this->error}</span>", E_USER_WARNING);
            } else {
                $out = ("<small>[Aff:{$this->affected} Num:{$this->numRows} Took:{$this->took}ms]</small>");
            }
            pr(sprintf('<p style = "text-align:left"><b>Query:</b> %s %s</p>', $sql, $out));
        }
    }
    /**
     * Gets full table name including prefix.
     *
     * @param mixed $model
     * @param bool  $quote
     *
     * @return string Full quoted table name
     */
    public function fullTableName($model, $quote = true)
    {
        if (is_object($model)) {
            $table = $model->tablePrefix.$model->table;
        } elseif (isset($this->config['prefix'])) {
            $table = $this->config['prefix'].strval($model);
        } else {
            $table = strval($model);
        }
        if ($quote) {
            return $this->name($table);
        }

        return $table;
    }
    /**
     * The "C" in CRUD.
     *
     * @param Model $model
     * @param array $fields
     * @param array $values
     *
     * @return bool Success
     */
    public function create(&$model, $fields = null, $values = null)
    {
        $id = null;

        if ($fields == null) {
            unset($fields, $values);
            $fields = array_keys($model->data);
            $values = array_values($model->data);
        }
        $count = count($fields);

        for ($i = 0; $i < $count; ++$i) {
            $valueInsert[] = $this->value($values[$i], $model->getColumnType($fields[$i]), false);
        }
        for ($i = 0; $i < $count; ++$i) {
            $fieldInsert[] = $this->name($fields[$i]);
            if ($fields[$i] == $model->primaryKey) {
                $id = $values[$i];
            }
        }
        $query = array(
            'table' => $this->fullTableName($model),
            'fields' => implode(', ', $fieldInsert),
            'values' => implode(', ', $valueInsert),
        );

        if ($this->execute($this->renderStatement('create', $query))) {
            if (empty($id)) {
                $id = $this->lastInsertId($this->fullTableName($model, false), $model->primaryKey);
            }
            $model->setInsertID($id);
            $model->id = $id;

            return true;
        } else {
            $model->onError();

            return false;
        }
    }
    /**
     * The "R" in CRUD.
     *
     * @param Model $model
     * @param array $queryData
     * @param int   $recursive Number of levels of association
     *
     * @return unknown
     */
    public function read(&$model, $queryData = array(), $recursive = null)
    {
        $queryData = $this->__scrubQueryData($queryData);

        $null = null;
        $array = array();
        $linkedModels = array();
        $this->__bypass = false;
        $this->__booleans = array();

        if ($recursive === null && isset($queryData['recursive'])) {
            $recursive = $queryData['recursive'];
        }

        if (!is_null($recursive)) {
            $_recursive = $model->recursive;
            $model->recursive = $recursive;
        }

        if (!empty($queryData['fields'])) {
            $this->__bypass = true;
            $queryData['fields'] = $this->fields($model, null, $queryData['fields']);
        } else {
            $queryData['fields'] = $this->fields($model);
        }

        foreach ($model->__associations as $type) {
            foreach ($model->{$type} as $assoc => $assocData) {
                if ($model->recursive > -1) {
                    $linkModel = &$model->{$assoc};
                    $external = isset($assocData['external']);

                    if ($model->useDbConfig == $linkModel->useDbConfig) {
                        if (true === $this->generateAssociationQuery($model, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null)) {
                            $linkedModels[] = $type.'/'.$assoc;
                        }
                    }
                }
            }
        }

        $query = $this->generateAssociationQuery($model, $null, null, null, null, $queryData, false, $null);

        $resultSet = $this->fetchAll($query, $model->cacheQueries, $model->alias);

        if ($resultSet === false) {
            $model->onError();

            return false;
        }

        $filtered = $this->__filterResults($resultSet, $model);

        if ($model->recursive > 0) {
            foreach ($model->__associations as $type) {
                foreach ($model->{$type} as $assoc => $assocData) {
                    $linkModel = &$model->{$assoc};

                    if (!in_array($type.'/'.$assoc, $linkedModels)) {
                        if ($model->useDbConfig == $linkModel->useDbConfig) {
                            $db = &$this;
                        } else {
                            $db = &ConnectionManager::getDataSource($linkModel->useDbConfig);
                        }
                    } elseif ($model->recursive > 1 && ($type == 'belongsTo' || $type == 'hasOne')) {
                        $db = &$this;
                    }

                    if (isset($db)) {
                        $stack = array($assoc);
                        $db->queryAssociation($model, $linkModel, $type, $assoc, $assocData, $array, true, $resultSet, $model->recursive - 1, $stack);
                        unset($db);
                    }
                }
            }
            $this->__filterResults($resultSet, $model, $filtered);
        }

        if (!is_null($recursive)) {
            $model->recursive = $_recursive;
        }

        return $resultSet;
    }
    /**
     * Private method.	Passes association results thru afterFind filters of corresponding model.
     *
     * @param array  $results  Reference of resultset to be filtered
     * @param object $model    Instance of model to operate against
     * @param array  $filtered List of classes already filtered, to be skipped
     *
     * @return return
     */
    public function __filterResults(&$results, &$model, $filtered = array())
    {
        $filtering = array();
        $count = count($results);

        for ($i = 0; $i < $count; ++$i) {
            if (is_array($results[$i])) {
                $classNames = array_keys($results[$i]);
                $count2 = count($classNames);

                for ($j = 0; $j < $count2; ++$j) {
                    $className = $classNames[$j];
                    if ($model->alias != $className && !in_array($className, $filtered)) {
                        if (!in_array($className, $filtering)) {
                            $filtering[] = $className;
                        }

                        if (isset($model->{$className}) && is_object($model->{$className})) {
                            $data = $model->{$className}->afterFind(array(array($className => $results[$i][$className])), false);
                        }
                        if (isset($data[0][$className])) {
                            $results[$i][$className] = $data[0][$className];
                        }
                    }
                }
            }
        }

        return $filtering;
    }
    /**
     * Enter description here...
     *
     * @param Model        $model
     * @param unknown_type $linkModel
     * @param string       $type        Association type
     * @param unknown_type $association
     * @param unknown_type $assocData
     * @param unknown_type $queryData
     * @param unknown_type $external
     * @param unknown_type $resultSet
     * @param int          $recursive   Number of levels of association
     * @param array        $stack
     */
    public function queryAssociation(&$model, &$linkModel, $type, $association, $assocData, &$queryData, $external = false, &$resultSet, $recursive, $stack)
    {
        if ($query = $this->generateAssociationQuery($model, $linkModel, $type, $association, $assocData, $queryData, $external, $resultSet)) {
            if (!isset($resultSet) || !is_array($resultSet)) {
                if (Configure::read() > 0) {
                    e('<div style = "font: Verdana bold 12px; color: #FF0000">'.sprintf(__('SQL Error in model %s:', true), $model->alias).' ');
                    if (isset($this->error) && $this->error != null) {
                        e($this->error);
                    }
                    e('</div>');
                }

                return;
            }
            $count = count($resultSet);

            if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) {
                $ins = $fetch = array();
                for ($i = 0; $i < $count; ++$i) {
                    if ($in = $this->insertQueryData('{$__cakeID__$}', $resultSet[$i], $association, $assocData, $model, $linkModel, $stack)) {
                        $ins[] = $in;
                    }
                }

                if (!empty($ins)) {
                    $fetch = $this->fetchAssociated($model, $query, $ins);
                }

                if (!empty($fetch) && is_array($fetch)) {
                    if ($recursive > 0) {
                        foreach ($linkModel->__associations as $type1) {
                            foreach ($linkModel->{$type1} as $assoc1 => $assocData1) {
                                $deepModel = &$linkModel->{$assoc1};
                                $tmpStack = $stack;
                                $tmpStack[] = $assoc1;

                                if ($linkModel->useDbConfig === $deepModel->useDbConfig) {
                                    $db = &$this;
                                } else {
                                    $db = &ConnectionManager::getDataSource($deepModel->useDbConfig);
                                }
                                $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack);
                            }
                        }
                    }
                }
                $this->__filterResults($fetch, $model);

                return $this->__mergeHasMany($resultSet, $fetch, $association, $model, $linkModel, $recursive);
            } elseif ($type === 'hasAndBelongsToMany') {
                $ins = $fetch = array();
                for ($i = 0; $i < $count; ++$i) {
                    if ($in = $this->insertQueryData('{$__cakeID__$}', $resultSet[$i], $association, $assocData, $model, $linkModel, $stack)) {
                        $ins[] = $in;
                    }
                }
                if (!empty($ins)) {
                    if (count($ins) > 1) {
                        $query = str_replace('{$__cakeID__$}', '('.implode(', ', $ins).')', $query);
                        $query = str_replace('= (', 'IN (', $query);
                        $query = str_replace('=	 (', 'IN (', $query);
                    } else {
                        $query = str_replace('{$__cakeID__$}', $ins[0], $query);
                    }

                    $query = str_replace('	WHERE 1 = 1', '', $query);
                }

                $foreignKey = $model->hasAndBelongsToMany[$association]['foreignKey'];
                $joinKeys = array($foreignKey, $model->hasAndBelongsToMany[$association]['associationForeignKey']);
                list($with, $habtmFields) = $model->joinModel($model->hasAndBelongsToMany[$association]['with'], $joinKeys);
                $habtmFieldsCount = count($habtmFields);
                $q = $this->insertQueryData($query, null, $association, $assocData, $model, $linkModel, $stack);

                if ($q != false) {
                    $fetch = $this->fetchAll($q, $model->cacheQueries, $model->alias);
                } else {
                    $fetch = null;
                }
            }

            for ($i = 0; $i < $count; ++$i) {
                $row = &$resultSet[$i];

                if ($type !== 'hasAndBelongsToMany') {
                    $q = $this->insertQueryData($query, $resultSet[$i], $association, $assocData, $model, $linkModel, $stack);
                    if ($q != false) {
                        $fetch = $this->fetchAll($q, $model->cacheQueries, $model->alias);
                    } else {
                        $fetch = null;
                    }
                }
                $selfJoin = false;

                if ($linkModel->name === $model->name) {
                    $selfJoin = true;
                }

                if (!empty($fetch) && is_array($fetch)) {
                    if ($recursive > 0) {
                        foreach ($linkModel->__associations as $type1) {
                            foreach ($linkModel->{$type1} as $assoc1 => $assocData1) {
                                $deepModel = &$linkModel->{$assoc1};

                                if (($type1 === 'belongsTo') || ($deepModel->alias === $model->alias && $type === 'belongsTo') || ($deepModel->alias != $model->alias)) {
                                    $tmpStack = $stack;
                                    $tmpStack[] = $assoc1;
                                    if ($linkModel->useDbConfig == $deepModel->useDbConfig) {
                                        $db = &$this;
                                    } else {
                                        $db = &ConnectionManager::getDataSource($deepModel->useDbConfig);
                                    }
                                    $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack);
                                }
                            }
                        }
                    }
                    if ($type == 'hasAndBelongsToMany') {
                        $uniqueIds = $merge = array();

                        foreach ($fetch as $j => $data) {
                            if (
                                (isset($data[$with]) && $data[$with][$foreignKey] === $row[$model->alias][$model->primaryKey]) &&
                                (!in_array($data[$with][$joinKeys[1]], $uniqueIds))
                            ) {
                                $uniqueIds[] = $data[$with][$joinKeys[1]];

                                if ($habtmFieldsCount <= 2) {
                                    unset($data[$with]);
                                }
                                $merge[] = $data;
                            }
                        }
                        if (empty($merge) && !isset($row[$association])) {
                            $row[$association] = $merge;
                        } else {
                            $this->__mergeAssociation($resultSet[$i], $merge, $association, $type);
                        }
                    } else {
                        $this->__mergeAssociation($resultSet[$i], $fetch, $association, $type, $selfJoin);
                    }
                    if (isset($resultSet[$i][$association])) {
                        $resultSet[$i][$association] = $linkModel->afterFind($resultSet[$i][$association]);
                    }
                } else {
                    $tempArray[0][$association] = false;
                    $this->__mergeAssociation($resultSet[$i], $tempArray, $association, $type, $selfJoin);
                }
            }
        }
    }
    /**
     * A more efficient way to fetch associations.	Woohoo!
     *
     * @param model  $model Primary model object
     * @param string $query Association query
     * @param array  $ids   Array of IDs of associated records
     *
     * @return array Association results
     */
    public function fetchAssociated($model, $query, $ids)
    {
        $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query);
        if (count($ids) > 1) {
            $query = str_replace('= (', 'IN (', $query);
            $query = str_replace('=	 (', 'IN (', $query);
        }

        return $this->fetchAll($query, $model->cacheQueries, $model->alias);
    }
    /**
     * mergeHasMany - Merge the results of hasMany relations.
     *
     *
     * @param array  $resultSet   Data to merge into
     * @param array  $merge       Data to merge
     * @param string $association Name of Model being Merged
     * @param object $model       Model being merged onto
     * @param object $linkModel   Model being merged
     **/
    public function __mergeHasMany(&$resultSet, $merge, $association, &$model, &$linkModel)
    {
        foreach ($resultSet as $i => $value) {
            $count = 0;
            $merged[$association] = array();
            foreach ($merge as $j => $data) {
                if (isset($value[$model->alias]) && $value[$model->alias][$model->primaryKey] === $data[$association][$model->hasMany[$association]['foreignKey']]) {
                    if (count($data) > 1) {
                        $data = array_merge($data[$association], $data);
                        unset($data[$association]);
                        foreach ($data as $key => $name) {
                            if (is_numeric($key)) {
                                $data[$association][] = $name;
                                unset($data[$key]);
                            }
                        }
                        $merged[$association][] = $data;
                    } else {
                        $merged[$association][] = $data[$association];
                    }
                }
                ++$count;
            }
            if (isset($value[$model->alias])) {
                $resultSet[$i] = Set::pushDiff($resultSet[$i], $merged);
                unset($merged);
            }
        }
    }
    /**
     * Enter description here...
     *
     * @param unknown_type $data
     * @param unknown_type $merge
     * @param unknown_type $association
     * @param unknown_type $type
     * @param bool         $selfJoin
     */
    public function __mergeAssociation(&$data, $merge, $association, $type, $selfJoin = false)
    {
        if (isset($merge[0]) && !isset($merge[0][$association])) {
            $association = Inflector::pluralize($association);
        }

        if ($type == 'belongsTo' || $type == 'hasOne') {
            if (isset($merge[$association])) {
                $data[$association] = $merge[$association][0];
            } else {
                if (count($merge[0][$association]) > 1) {
                    foreach ($merge[0] as $assoc => $data2) {
                        if ($assoc != $association) {
                            $merge[0][$association][$assoc] = $data2;
                        }
                    }
                }
                if (!isset($data[$association])) {
                    if ($merge[0][$association] != null) {
                        $data[$association] = $merge[0][$association];
                    } else {
                        $data[$association] = array();
                    }
                } else {
                    if (is_array($merge[0][$association])) {
                        foreach ($data[$association] as $k => $v) {
                            if (!is_array($v)) {
                                $dataAssocTmp[$k] = $v;
                            }
                        }

                        foreach ($merge[0][$association] as $k => $v) {
                            if (!is_array($v)) {
                                $mergeAssocTmp[$k] = $v;
                            }
                        }
                        $dataKeys = array_keys($data);
                        $mergeKeys = array_keys($merge[0]);

                        if ($mergeKeys[0] === $dataKeys[0] || $mergeKeys === $dataKeys) {
                            $data[$association][$association] = $merge[0][$association];
                        } else {
                            $diff = Set::diff($dataAssocTmp, $mergeAssocTmp);
                            $data[$association] = array_merge($merge[0][$association], $diff);
                        }
                    } elseif ($selfJoin && array_key_exists($association, $merge[0])) {
                        $data[$association] = array_merge($data[$association], array($association => array()));
                    }
                }
            }
        } else {
            if (isset($merge[0][$association]) && $merge[0][$association] === false) {
                if (!isset($data[$association])) {
                    $data[$association] = array();
                }
            } else {
                foreach ($merge as $i => $row) {
                    if (count($row) == 1) {
                        if (empty($data[$association]) || (isset($data[$association]) && !in_array($row[$association], $data[$association]))) {
                            $data[$association][] = $row[$association];
                        }
                    } elseif (!empty($row)) {
                        $tmp = array_merge($row[$association], $row);
                        unset($tmp[$association]);
                        $data[$association][] = $tmp;
                    }
                }
            }
        }
    }
    /**
     * Generates an array representing a query or part of a query from a single model or two associated models.
     *
     * @param Model  $model
     * @param Model  $linkModel
     * @param string $type
     * @param string $association
     * @param array  $assocData
     * @param array  $queryData
     * @param bool   $external
     * @param array  $resultSet
     *
     * @return mixed
     */
    public function generateAssociationQuery(&$model, &$linkModel, $type, $association = null, $assocData = array(), &$queryData, $external = false, &$resultSet)
    {
        $queryData = $this->__scrubQueryData($queryData);
        $assocData = $this->__scrubQueryData($assocData);

        if (empty($queryData['fields'])) {
            $queryData['fields'] = $this->fields($model, $model->alias);
        } elseif (!empty($model->hasMany) && $model->recursive > -1) {
            $assocFields = $this->fields($model, $model->alias, array("{$model->alias}.{$model->primaryKey}"));
            $passedFields = $this->fields($model, $model->alias, $queryData['fields']);

            if (count($passedFields) === 1) {
                $match = strpos($passedFields[0], $assocFields[0]);
                $match1 = strpos($passedFields[0], 'COUNT(');
                if ($match === false && $match1 === false) {
                    $queryData['fields'] = array_merge($passedFields, $assocFields);
                } else {
                    $queryData['fields'] = $passedFields;
                }
            } else {
                $queryData['fields'] = array_merge($passedFields, $assocFields);
            }
            unset($assocFields, $passedFields);
        }

        if ($linkModel == null) {
            return $this->buildStatement(
                array(
                    'fields' => array_unique($queryData['fields']),
                    'table' => $this->fullTableName($model),
                    'alias' => $model->alias,
                    'limit' => $queryData['limit'],
                    'offset' => $queryData['offset'],
                    'joins' => $queryData['joins'],
                    'conditions' => $queryData['conditions'],
                    'order' => $queryData['order'],
                    'group' => $queryData['group'],
                ),
                $model
            );
        }
        if ($external && !empty($assocData['finderQuery'])) {
            return $assocData['finderQuery'];
        }

        $alias = $association;
        $self = ($model->name == $linkModel->name);
        $fields = array();

        if ((!$external && in_array($type, array('hasOne', 'belongsTo')) && $this->__bypass === false) || $external) {
            $fields = $this->fields($linkModel, $alias, $assocData['fields']);
        }
        if (empty($assocData['offset']) && !empty($assocData['page'])) {
            $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit'];
        }
        $assocData['limit'] = $this->limit($assocData['limit'], $assocData['offset']);

        switch ($type) {
            case 'hasOne':
            case 'belongsTo':
                $conditions = $this->__mergeConditions(
                    $assocData['conditions'],
                    $this->getConstraint($type, $model, $linkModel, $alias, array_merge($assocData, compact('external', 'self')))
                );

                if (!$self && $external) {
                    foreach ($conditions as $key => $condition) {
                        if (is_numeric($key) && strpos($condition, $model->alias.'.') !== false) {
                            unset($conditions[$key]);
                        }
                    }
                }

                if ($external) {
                    $query = array_merge($assocData, array(
                        'conditions' => $conditions,
                        'table' => $this->fullTableName($linkModel),
                        'fields' => $fields,
                        'alias' => $alias,
                        'group' => null,
                    ));
                    $query = array_merge(array('order' => $assocData['order'], 'limit' => $assocData['limit']), $query);
                } else {
                    $join = array(
                        'table' => $this->fullTableName($linkModel),
                        'alias' => $alias,
                        'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT',
                        'conditions' => trim($this->conditions($conditions, true, false, $model)),
                    );
                    $queryData['fields'] = array_merge($queryData['fields'], $fields);

                    if (!empty($assocData['order'])) {
                        $queryData['order'][] = $assocData['order'];
                    }
                    if (!in_array($join, $queryData['joins'])) {
                        $queryData['joins'][] = $join;
                    }

                    return true;
                }
            break;
            case 'hasMany':
                $assocData['fields'] = $this->fields($linkModel, $alias, $assocData['fields']);
                if (!empty($assocData['foreignKey'])) {
                    $assocData['fields'] = array_merge($assocData['fields'], $this->fields($linkModel, $alias, array("{$alias}.{$assocData['foreignKey']}")));
                }
                $query = array(
                    'conditions' => $this->__mergeConditions($this->getConstraint('hasMany', $model, $linkModel, $alias, $assocData), $assocData['conditions']),
                    'fields' => array_unique($assocData['fields']),
                    'table' => $this->fullTableName($linkModel),
                    'alias' => $alias,
                    'order' => $assocData['order'],
                    'limit' => $assocData['limit'],
                    'group' => null,
                );
            break;
            case 'hasAndBelongsToMany':
                $joinFields = array();
                $joinAssoc = null;

                if (isset($assocData['with']) && !empty($assocData['with'])) {
                    $joinKeys = array($assocData['foreignKey'], $assocData['associationForeignKey']);
                    list($with, $joinFields) = $model->joinModel($assocData['with'], $joinKeys);

                    $joinTbl = $this->fullTableName($model->{$with});
                    $joinAlias = $joinTbl;

                    if (is_array($joinFields) && !empty($joinFields)) {
                        $joinFields = $this->fields($model->{$with}, $model->{$with}->alias, $joinFields);
                        $joinAssoc = $joinAlias = $model->{$with}->alias;
                    } else {
                        $joinFields = array();
                    }
                } else {
                    $joinTbl = $this->fullTableName($assocData['joinTable']);
                    $joinAlias = $joinTbl;
                }
                $query = array(
                    'conditions' => $assocData['conditions'],
                    'limit' => $assocData['limit'],
                    'table' => $this->fullTableName($linkModel),
                    'alias' => $alias,
                    'fields' => array_merge($this->fields($linkModel, $alias, $assocData['fields']), $joinFields),
                    'order' => $assocData['order'],
                    'group' => null,
                    'joins' => array(array(
                        'table' => $joinTbl,
                        'alias' => $joinAssoc,
                        'conditions' => $this->getConstraint('hasAndBelongsToMany', $model, $linkModel, $joinAlias, $assocData, $alias),
                    )),
                );
            break;
        }
        if (isset($query)) {
            return $this->buildStatement($query, $model);
        }

        return;
    }
    /**
     * Returns a conditions array for the constraint between two models.
     *
     * @param string $type        Association type
     * @param object $model       Model object
     * @param array  $association Association array
     *
     * @return array Conditions array defining the constraint between $model and $association
     */
    public function getConstraint($type, $model, $linkModel, $alias, $assoc, $alias2 = null)
    {
        $assoc = array_merge(array('external' => false, 'self' => false), $assoc);

        if (array_key_exists('foreignKey', $assoc) && empty($assoc['foreignKey'])) {
            return array();
        }

        switch (true) {
            case ($assoc['external'] && $type == 'hasOne'):
                return array("{$alias}.{$assoc['foreignKey']}" => '{$__cakeID__$}');
            break;
            case ($assoc['external'] && $type == 'belongsTo'):
                return array("{$alias}.{$linkModel->primaryKey}" => '{$__cakeForeignKey__$}');
            break;
            case (!$assoc['external'] && $type == 'hasOne'):
                return array("{$alias}.{$assoc['foreignKey']}" => $this->identifier("{$model->alias}.{$model->primaryKey}"));
            break;
            case (!$assoc['external'] && $type == 'belongsTo'):
                return array("{$model->alias}.{$assoc['foreignKey']}" => $this->identifier("{$alias}.{$linkModel->primaryKey}"));
            break;
            case ($type == 'hasMany'):
                return array("{$alias}.{$assoc['foreignKey']}" => array('{$__cakeID__$}'));
            break;
            case ($type == 'hasAndBelongsToMany'):
                return array(
                    array("{$alias}.{$assoc['foreignKey']}" => '{$__cakeID__$}'),
                    array("{$alias}.{$assoc['associationForeignKey']}" => $this->identifier("{$alias2}.{$linkModel->primaryKey}")),
                );
            break;
        }

        return array();
    }
    /**
     * Builds and generates a JOIN statement from an array.	 Handles final clean-up before conversion.
     *
     * @param array $join An array defining a JOIN statement in a query
     *
     * @return string An SQL JOIN statement to be used in a query
     *
     * @see DboSource::renderJoinStatement()
     * @see DboSource::buildStatement()
     */
    public function buildJoinStatement($join)
    {
        $data = array_merge(array(
            'type' => null,
            'alias' => null,
            'table' => 'join_table',
            'conditions' => array(),
        ), $join);

        if (!empty($data['alias'])) {
            $data['alias'] = $this->alias.$this->name($data['alias']);
        }
        if (!empty($data['conditions'])) {
            $data['conditions'] = trim($this->conditions($data['conditions'], true, false));
        }

        return $this->renderJoinStatement($data);
    }
    /**
     * Builds and generates an SQL statement from an array.	 Handles final clean-up before conversion.
     *
     * @param array  $query An array defining an SQL query
     * @param object $model The model object which initiated the query
     *
     * @return string An executable SQL statement
     *
     * @see DboSource::renderStatement()
     */
    public function buildStatement($query, $model)
    {
        $query = array_merge(array('offset' => null, 'joins' => array()), $query);
        if (!empty($query['joins'])) {
            $count = count($query['joins']);
            for ($i = 0; $i < $count; ++$i) {
                if (is_array($query['joins'][$i])) {
                    $query['joins'][$i] = $this->buildJoinStatement($query['joins'][$i]);
                }
            }
        }

        return $this->renderStatement('select', array(
            'conditions' => $this->conditions($query['conditions'], true, true, $model),
            'fields' => implode(', ', $query['fields']),
            'table' => $query['table'],
            'alias' => $this->alias.$this->name($query['alias']),
            'order' => $this->order($query['order']),
            'limit' => $this->limit($query['limit'], $query['offset']),
            'joins' => implode(' ', $query['joins']),
            'group' => $this->group($query['group']),
        ));
    }
    /**
     * Renders a final SQL JOIN statement.
     *
     * @param array $data
     *
     * @return string
     */
    public function renderJoinStatement($data)
    {
        extract($data);

        return trim("{$type} JOIN {$table} {$alias} ON ({$conditions})");
    }
    /**
     * Renders a final SQL statement by putting together the component parts in the correct order.
     *
     * @param string $type
     * @param array  $data
     *
     * @return string
     */
    public function renderStatement($type, $data)
    {
        extract($data);
        $aliases = null;

        switch (strtolower($type)) {
            case 'select':
                return "SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}";
            break;
            case 'create':
                return "INSERT INTO {$table} ({$fields}) VALUES ({$values})";
            break;
            case 'update':
                if (!empty($alias)) {
                    $aliases = "{$this->alias}{$alias} {$joins} ";
                }

                return "UPDATE {$table} {$aliases}SET {$fields} {$conditions}";
            break;
            case 'delete':
                if (!empty($alias)) {
                    $aliases = "{$this->alias}{$alias} {$joins} ";
                }

                return "DELETE {$alias} FROM {$table} {$aliases}{$conditions}";
            break;
            case 'schema':
                foreach (array('columns', 'indexes') as $var) {
                    if (is_array(${$var})) {
                        ${$var} = "\t".implode(",\n\t", array_filter(${$var}));
                    }
                }
                if (trim($indexes) != '') {
                    $columns .= ',';
                }

                return "CREATE TABLE {$table} (\n{$columns}{$indexes});";
            break;
            case 'alter':
            break;
        }
    }
    /**
     * Merges a mixed set of string/array conditions.
     *
     * @return array
     */
    public function __mergeConditions($query, $assoc)
    {
        if (empty($assoc)) {
            return $query;
        }

        if (is_array($query)) {
            return array_merge((array) $assoc, $query);
        }

        if (!empty($query)) {
            $query = array($query);
            if (is_array($assoc)) {
                $query = array_merge($query, $assoc);
            } else {
                $query[] = $assoc;
            }

            return $query;
        }

        return $assoc;
    }
    /**
     * Generates and executes an SQL UPDATE statement for given model, fields, and values.
     * For databases that do not support aliases in UPDATE queries.
     *
     * @param Model $model
     * @param array $fields
     * @param array $values
     * @param mixed $conditions
     *
     * @return bool Success
     */
    public function update(&$model, $fields = array(), $values = null, $conditions = null)
    {
        if ($values == null) {
            $combined = $fields;
        } else {
            $combined = array_combine($fields, $values);
        }
        $fields = implode(', ', $this->_prepareUpdateFields($model, $combined, empty($conditions)));

        $alias = $joins = null;
        $table = $this->fullTableName($model);
        $conditions = $this->_matchRecords($model, $conditions);

        if ($conditions === false) {
            return false;
        }
        $query = compact('table', 'alias', 'joins', 'fields', 'conditions');

        if (!$this->execute($this->renderStatement('update', $query))) {
            $model->onError();

            return false;
        }

        return true;
    }
    /**
     * Quotes and prepares fields and values for an SQL UPDATE statement.
     *
     * @param Model $model
     * @param array $fields
     * @param bool  $quoteValues If values should be quoted, or treated as SQL snippets
     * @param bool  $alias       Include the model alias in the field name
     *
     * @return array Fields and values, quoted and preparted
     */
    public function _prepareUpdateFields(&$model, $fields, $quoteValues = true, $alias = false)
    {
        $quotedAlias = $this->startQuote.$model->alias.$this->endQuote;

        foreach ($fields as $field => $value) {
            if ($alias && strpos($field, '.') === false) {
                $quoted = $model->escapeField($field);
            } elseif (!$alias && strpos($field, '.') !== false) {
                $quoted = $this->name(str_replace($quotedAlias.'.', '', str_replace(
                    $model->alias.'.', '', $field
                )));
            } else {
                $quoted = $this->name($field);
            }

            if ($value === null) {
                $updates[] = $quoted.' = NULL';
                continue;
            }
            $update = $quoted.' = ';

            if ($quoteValues) {
                $update .= $this->value($value, $model->getColumnType($field), false);
            } elseif (!$alias) {
                $update .= str_replace($quotedAlias.'.', '', str_replace(
                    $model->alias.'.', '', $value
                ));
            } else {
                $update .= $value;
            }
            $updates[] = $update;
        }

        return $updates;
    }
    /**
     * Generates and executes an SQL DELETE statement.
     * For databases that do not support aliases in UPDATE queries.
     *
     * @param Model $model
     * @param mixed $conditions
     *
     * @return bool Success
     */
    public function delete(&$model, $conditions = null)
    {
        $alias = $joins = null;
        $table = $this->fullTableName($model);
        $conditions = $this->_matchRecords($model, $conditions);

        if ($conditions === false) {
            return false;
        }

        if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) {
            $model->onError();

            return false;
        }

        return true;
    }
    /**
     * Gets a list of record IDs for the given conditions.	Used for multi-record updates and deletes
     * in databases that do not support aliases in UPDATE/DELETE queries.
     *
     * @param Model $model
     * @param mixed $conditions
     *
     * @return array List of record IDs
     */
    public function _matchRecords(&$model, $conditions = null)
    {
        if ($conditions === true) {
            $conditions = $this->conditions(true);
        } elseif ($conditions === null) {
            $conditions = $this->conditions($this->defaultConditions($model, $conditions, false), true, true, $model);
        } else {
            $idList = $model->find('all', array(
                'fields' => "{$model->alias}.{$model->primaryKey}",
                'conditions' => $conditions,
            ));

            if (empty($idList)) {
                return false;
            }
            $conditions = $this->conditions(array(
                $model->primaryKey => Set::extract($idList, "{n}.{$model->alias}.{$model->primaryKey}"),
            ));
        }

        return $conditions;
    }
    /**
     * Returns an array of SQL JOIN fragments from a model's associations.
     *
     * @param object $model
     *
     * @return array
     */
    public function _getJoins($model)
    {
        $join = array();
        $joins = array_merge($model->getAssociated('hasOne'), $model->getAssociated('belongsTo'));

        foreach ($joins as $assoc) {
            if (isset($model->{$assoc}) && $model->useDbConfig == $model->{$assoc}->useDbConfig) {
                $assocData = $model->getAssociated($assoc);
                $join[] = $this->buildJoinStatement(array(
                    'table' => $this->fullTableName($model->{$assoc}),
                    'alias' => $assoc,
                    'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT',
                    'conditions' => trim($this->conditions(
                        $this->getConstraint($assocData['association'], $model, $model->{$assoc}, $assoc, $assocData),
                        true, false, $model
                    )),
                ));
            }
        }

        return $join;
    }
    /**
     * Returns the an SQL calculation, i.e. COUNT() or MAX().
     *
     * @param model  $model
     * @param string $func   Lowercase name of SQL function, i.e. 'count' or 'max'
     * @param array  $params Function parameters (any values must be quoted manually)
     *
     * @return string An SQL calculation function
     */
    public function calculate(&$model, $func, $params = array())
    {
        $params = (array) $params;

        switch (strtolower($func)) {
            case 'count':
                if (!isset($params[0])) {
                    $params[0] = '*';
                }
                if (!isset($params[1])) {
                    $params[1] = 'count';
                }

                return 'COUNT('.$this->name($params[0]).') AS '.$this->name($params[1]);
            case 'max':
            case 'min':
                if (!isset($params[1])) {
                    $params[1] = $params[0];
                }

                return strtoupper($func).'('.$this->name($params[0]).') AS '.$this->name($params[1]);
            break;
        }
    }
    /**
     * Deletes all the records in a table and resets the count of the auto-incrementing
     * primary key, where applicable.
     *
     * @param mixed $table A string or model class representing the table to be truncated
     *
     * @return bool SQL TRUNCATE TABLE statement, false if not applicable.
     */
    public function truncate($table)
    {
        return $this->execute('TRUNCATE TABLE '.$this->fullTableName($table));
    }
    /**
     * Begin a transaction.
     *
     * @param model $model
     *
     * @return bool True on success, false on fail
     *              (i.e. if the database/model does not support transactions,
     *              or a transaction has not started).
     */
    public function begin(&$model)
    {
        if (parent::begin($model) && $this->execute($this->_commands['begin'])) {
            $this->_transactionStarted = true;

            return true;
        }

        return false;
    }
    /**
     * Commit a transaction.
     *
     * @param model $model
     *
     * @return bool True on success, false on fail
     *              (i.e. if the database/model does not support transactions,
     *              or a transaction has not started).
     */
    public function commit(&$model)
    {
        if (parent::commit($model) && $this->execute($this->_commands['commit'])) {
            $this->_transactionStarted = false;

            return true;
        }

        return false;
    }
    /**
     * Rollback a transaction.
     *
     * @param model $model
     *
     * @return bool True on success, false on fail
     *              (i.e. if the database/model does not support transactions,
     *              or a transaction has not started).
     */
    public function rollback(&$model)
    {
        if (parent::rollback($model) && $this->execute($this->_commands['rollback'])) {
            $this->_transactionStarted = false;

            return true;
        }

        return false;
    }
    /**
     * Creates a default set of conditions from the model if $conditions is null/empty.
     *
     * @param object $model
     * @param mixed  $conditions
     * @param bool   $useAlias   Use model aliases rather than table names when generating conditions
     *
     * @return mixed
     */
    public function defaultConditions(&$model, $conditions, $useAlias = true)
    {
        if (!empty($conditions)) {
            return $conditions;
        }
        if (!$model->exists()) {
            return false;
        }
        $alias = $model->alias;

        if (!$useAlias) {
            $alias = $this->fullTableName($model, false);
        }

        return array("{$alias}.{$model->primaryKey}" => $model->getID());
    }
    /**
     * Returns a key formatted like a string Model.fieldname(i.e. Post.title, or Country.name).
     *
     * @param unknown_type $model
     * @param unknown_type $key
     * @param unknown_type $assoc
     *
     * @return string
     */
    public function resolveKey($model, $key, $assoc = null)
    {
        if (empty($assoc)) {
            $assoc = $model->alias;
        }
        if (!strpos('.', $key)) {
            return $this->name($model->alias).'.'.$this->name($key);
        }

        return $key;
    }
    /**
     * Private helper method to remove query metadata in given data array.
     *
     * @param array $data
     *
     * @return array
     */
    public function __scrubQueryData($data)
    {
        foreach (array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group') as $key) {
            if (!isset($data[$key]) || empty($data[$key])) {
                $data[$key] = array();
            }
        }

        return $data;
    }
    /**
     * Generates the fields list of an SQL query.
     *
     * @param Model  $model
     * @param string $alias  Alias tablename
     * @param mixed  $fields
     * @param bool   $quote  If false, returns fields array unquoted
     *
     * @return array
     */
    public function fields(&$model, $alias = null, $fields = array(), $quote = true)
    {
        if (empty($alias)) {
            $alias = $model->alias;
        }
        if (empty($fields)) {
            $fields = array_keys($model->schema());
        } elseif (!is_array($fields)) {
            $fields = String::tokenize($fields);
        }
        $fields = array_values(array_filter($fields));

        if (!$quote) {
            return $fields;
        }
        $count = count($fields);

        if ($count >= 1 && !in_array($fields[0], array('*', 'COUNT(*)'))) {
            for ($i = 0; $i < $count; ++$i) {
                if (!preg_match('/^.+\\(.*\\)/', $fields[$i])) {
                    $prepend = '';

                    if (strpos($fields[$i], 'DISTINCT') !== false) {
                        $prepend = 'DISTINCT ';
                        $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));
                    }
                    $dot = strpos($fields[$i], '.');

                    if ($dot === false) {
                        $prefix = !(
                            strpos($fields[$i], ' ') !== false ||
                            strpos($fields[$i], '(') !== false
                        );
                        $fields[$i] = $this->name(($prefix ? $alias.'.' : '').$fields[$i]);
                    } else {
                        $value = array();
                        $comma = strpos($fields[$i], ',');
                        if ($comma === false) {
                            $build = explode('.', $fields[$i]);
                            if (!Set::numeric($build)) {
                                $fields[$i] = $this->name($build[0].'.'.$build[1]);
                            }
                            $comma = String::tokenize($fields[$i]);
                            foreach ($comma as $string) {
                                if (preg_match('/^[0-9]+\.[0-9]+$/', $string)) {
                                    $value[] = $string;
                                } else {
                                    $build = explode('.', $string);
                                    $value[] = $this->name(trim($build[0]).'.'.trim($build[1]));
                                }
                            }
                            $fields[$i] = implode(', ', $value);
                        }
                    }
                    $fields[$i] = $prepend.$fields[$i];
                } elseif (preg_match('/\(([\.\w]+)\)/', $fields[$i], $field)) {
                    if (isset($field[1])) {
                        if (strpos($field[1], '.') === false) {
                            $field[1] = $this->name($alias.'.'.$field[1]);
                        } else {
                            $field[0] = explode('.', $field[1]);
                            if (!Set::numeric($field[0])) {
                                $field[0] = implode('.', array_map(array($this, 'name'), $field[0]));
                                $fields[$i] = preg_replace('/\('.$field[1].'\)/', '('.$field[0].')', $fields[$i], 1);
                            }
                        }
                    }
                }
            }
        }

        return array_unique($fields);
    }
    /**
     * Creates a WHERE clause by parsing given conditions data.
     *
     * @param mixed $conditions  Array or string of conditions
     * @param bool  $quoteValues If true, values should be quoted
     * @param bool  $where       If true, "WHERE " will be prepended to the return value
     * @param Model $model       A reference to the Model instance making the query
     *
     * @return string SQL fragment
     */
    public function conditions($conditions, $quoteValues = true, $where = true, $model = null)
    {
        $clause = $out = '';

        if ($where) {
            $clause = ' WHERE ';
        }

        if (is_array($conditions) && !empty($conditions)) {
            $out = $this->conditionKeysToString($conditions, $quoteValues, $model);

            if (empty($out)) {
                return $clause.' 1 = 1';
            }

            return $clause.implode(' AND ', $out);
        }

        if (empty($conditions) || trim($conditions) == '' || $conditions === true) {
            return $clause.'1 = 1';
        }
        $clauses = '/^WHERE\\x20|^GROUP\\x20BY\\x20|^HAVING\\x20|^ORDER\\x20BY\\x20/i';

        if (preg_match($clauses, $conditions, $match)) {
            $clause = '';
        }
        if (trim($conditions) == '') {
            $conditions = ' 1 = 1';
        } else {
            $conditions = $this->__quoteFields($conditions);
        }

        return $clause.$conditions;
    }
    /**
     * Creates a WHERE clause by parsing given conditions array.  Used by DboSource::conditions().
     *
     * @param array $conditions  Array or string of conditions
     * @param bool  $quoteValues If true, values should be quoted
     * @param Model $model       A reference to the Model instance making the query
     *
     * @return string SQL fragment
     */
    public function conditionKeysToString($conditions, $quoteValues = true, $model = null)
    {
        $c = 0;
        $out = array();
        $data = $columnType = null;
        $bool = array('and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&');

        foreach ($conditions as $key => $value) {
            $join = ' AND ';
            $not = null;

            if (is_array($value)) {
                $valueInsert = (
                    !empty($value) &&
                    (substr_count($key, '?') == count($value) || substr_count($key, ':') == count($value))
                );
            }

            if (is_numeric($key) && empty($value)) {
                continue;
            } elseif (is_numeric($key) && is_string($value)) {
                $out[] = $not.$this->__quoteFields($value);
            } elseif ((is_numeric($key) && is_array($value)) || in_array(strtolower(trim($key)), $bool)) {
                if (in_array(strtolower(trim($key)), $bool)) {
                    $join = ' '.strtoupper($key).' ';
                } else {
                    $key = $join;
                }
                $value = $this->conditionKeysToString($value, $quoteValues, $model);

                if (strpos($join, 'NOT') !== false) {
                    if (strtoupper(trim($key)) == 'NOT') {
                        $key = 'AND '.trim($key);
                    }
                    $not = 'NOT ';
                }

                if (empty($value[1])) {
                    if ($not) {
                        $out[] = $not.'('.$value[0].')';
                    } else {
                        $out[] = $value[0];
                    }
                } else {
                    $out[] = '('.$not.'('.implode(') '.strtoupper($key).' (', $value).'))';
                }
            } else {
                if (is_object($value) && isset($value->type)) {
                    if ($value->type == 'identifier') {
                        $data .= $this->name($key).' = '.$this->name($value->value);
                    } elseif ($value->type == 'expression') {
                        if (is_numeric($key)) {
                            $data .= $value->value;
                        } else {
                            $data .= $this->name($key).' = '.$value->value;
                        }
                    }
                } elseif (is_array($value) && !empty($value) && !$valueInsert) {
                    $keys = array_keys($value);
                    if (array_keys($value) === array_values(array_keys($value))) {
                        $count = count($value);
                        if ($count === 1) {
                            $data = $this->name($key).' = (';
                        } else {
                            $data = $this->name($key).' IN (';
                        }
                        if ($quoteValues || strpos($value[0], '-!') !== 0) {
                            if (is_object($model)) {
                                $columnType = $model->getColumnType($key);
                            }
                            $data .= implode(', ', $this->value($value, $columnType));
                        }
                        $data .= ')';
                    } else {
                        $ret = $this->conditionKeysToString($value, $quoteValues, $model);
                        if (count($ret) > 1) {
                            $data = '('.implode(') AND (', $ret).')';
                        } elseif (isset($ret[0])) {
                            $data = $ret[0];
                        }
                    }
                } elseif (is_numeric($key) && !empty($value)) {
                    $data = $this->__quoteFields($value);
                } else {
                    $data = $this->__parseKey($model, trim($key), $value);
                }

                if ($data != null) {
                    if (preg_match('/^\(\(\((.+)\)\)\)$/', $data)) {
                        $data = substr($data, 1, strlen($data) - 2);
                    }
                    $out[] = $data;
                    $data = null;
                }
            }
            ++$c;
        }

        return $out;
    }
    /**
     * Extracts a Model.field identifier and an SQL condition operator from a string, formats
     * and inserts values, and composes them into an SQL snippet.
     *
     * @param Model  $model Model object initiating the query
     * @param string $key   An SQL key snippet containing a field and optional SQL operator
     * @param mixed  $value The value(s) to be inserted in the string
     *
     * @return string
     */
    public function __parseKey($model, $key, $value)
    {
        $operatorMatch = '/^(('.implode(')|(', $this->__sqlOps);
        $operatorMatch .= '\\x20)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is';
        $bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false));

        if (!strpos($key, ' ')) {
            $operator = '=';
        } else {
            list($key, $operator) = explode(' ', trim($key), 2);

            if (!preg_match($operatorMatch, trim($operator)) && strpos($operator, ' ') !== false) {
                $key = $key.' '.$operator;
                $split = strrpos($key, ' ');
                $operator = substr($key, $split);
                $key = substr($key, 0, $split);
            }
        }

        $type = (is_object($model) ? $model->getColumnType($key) : null);
        $null = ($value === null || (is_array($value) && empty($value)));

        if (strtolower($operator) === 'not') {
            $data = $this->conditionKeysToString(
                array($operator => array($key => $value)), true, $model
            );

            return $data[0];
        }
        $value = $this->value($value, $type);

        if ($key !== '?') {
            $isKey = (strpos($key, '(') !== false || strpos($key, ')') !== false);
            $key = $isKey ? $this->__quoteFields($key) : $this->name($key);
        }

        if ($bound) {
            return String::insert($key.' '.trim($operator), $value);
        }

        if (!preg_match($operatorMatch, trim($operator))) {
            $operator .= ' =';
        }
        $operator = trim($operator);

        if (is_array($value)) {
            $value = implode(', ', $value);

            switch ($operator) {
                case '=':
                    $operator = 'IN';
                break;
                case '!=':
                case '<>':
                    $operator = 'NOT IN';
                break;
            }
            $value = "({$value})";
        } elseif ($null) {
            switch ($operator) {
                case '=':
                    $operator = 'IS';
                break;
                case '!=':
                case '<>':
                    $operator = 'IS NOT';
                break;
            }
        }

        return "{$key} {$operator} {$value}";
    }
    /**
     * Quotes Model.fields.
     *
     * @param string $conditions
     *
     * @return string or false if no match
     */
    public function __quoteFields($conditions)
    {
        $start = $end = null;
        $original = $conditions;

        if (!empty($this->startQuote)) {
            $start = preg_quote($this->startQuote);
        }
        if (!empty($this->endQuote)) {
            $end = preg_quote($this->endQuote);
        }
        $conditions = str_replace(array($start, $end), '', $conditions);
        preg_match_all('/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_'.$start.$end.']*\\.[a-z0-9_'.$start.$end.']*)/i', $conditions, $replace, PREG_PATTERN_ORDER);

        if (isset($replace['1']['0'])) {
            $pregCount = count($replace['1']);

            for ($i = 0; $i < $pregCount; ++$i) {
                if (!empty($replace['1'][$i]) && !is_numeric($replace['1'][$i])) {
                    $conditions = preg_replace('/\b'.preg_quote($replace['1'][$i]).'\b/', $this->name($replace['1'][$i]), $conditions);
                }
            }

            return $conditions;
        }

        return $original;
    }
    /**
     * Returns a limit statement in the correct format for the particular database.
     *
     * @param int $limit  Limit of results returned
     * @param int $offset Offset from which to start results
     *
     * @return string SQL limit/offset statement
     */
    public function limit($limit, $offset = null)
    {
        if ($limit) {
            $rt = '';
            if (!strpos(strtolower($limit), 'limit') || strpos(strtolower($limit), 'limit') === 0) {
                $rt = ' LIMIT';
            }

            if ($offset) {
                $rt .= ' '.$offset.',';
            }

            $rt .= ' '.$limit;

            return $rt;
        }

        return;
    }
    /**
     * Returns an ORDER BY clause as a string.
     *
     * @param string $key       Field reference, as a key (i.e. Post.title)
     * @param string $direction Direction (ASC or DESC)
     *
     * @return string ORDER BY clause
     */
    public function order($keys, $direction = 'ASC')
    {
        if (is_string($keys) && strpos($keys, ',') && !preg_match('/\(.+\,.+\)/', $keys)) {
            $keys = array_map('trim', explode(',', $keys));
        }

        if (is_array($keys)) {
            $keys = array_filter($keys);
        }

        if (empty($keys) || (is_array($keys) && count($keys) && isset($keys[0]) && empty($keys[0]))) {
            return '';
        }

        if (is_array($keys)) {
            $keys = (Set::countDim($keys) > 1) ? array_map(array(&$this, 'order'), $keys) : $keys;

            foreach ($keys as $key => $value) {
                if (is_numeric($key)) {
                    $key = $value = ltrim(str_replace('ORDER BY ', '', $this->order($value)));
                    $value = (!preg_match('/\\x20ASC|\\x20DESC/i', $key) ? ' '.$direction : '');
                } else {
                    $value = ' '.$value;
                }

                if (!preg_match('/^.+\\(.*\\)/', $key) && !strpos($key, ',')) {
                    if (preg_match('/\\x20ASC|\\x20DESC/i', $key, $dir)) {
                        $dir = $dir[0];
                        $key = preg_replace('/\\x20ASC|\\x20DESC/i', '', $key);
                    } else {
                        $dir = '';
                    }
                    $key = trim($key);
                    if (!preg_match('/\s/', $key)) {
                        $key = $this->name($key);
                    }
                    $key .= ' '.trim($dir);
                }
                $order[] = $this->order($key.$value);
            }

            return ' ORDER BY '.trim(str_replace('ORDER BY', '', implode(',', $order)));
        }
        $keys = preg_replace('/ORDER\\x20BY/i', '', $keys);

        if (strpos($keys, '.')) {
            preg_match_all('/([a-zA-Z0-9_]{1,})\\.([a-zA-Z0-9_]{1,})/', $keys, $result, PREG_PATTERN_ORDER);
            $pregCount = count($result[0]);

            for ($i = 0; $i < $pregCount; ++$i) {
                if (!is_numeric($result[0][$i])) {
                    $keys = preg_replace('/'.$result[0][$i].'/', $this->name($result[0][$i]), $keys);
                }
            }
            $result = ' ORDER BY '.$keys;

            return $result.(!preg_match('/\\x20ASC|\\x20DESC/i', $keys) ? ' '.$direction : '');
        } elseif (preg_match('/(\\x20ASC|\\x20DESC)/i', $keys, $match)) {
            $direction = $match[1];

            return ' ORDER BY '.preg_replace('/'.$match[1].'/', '', $keys).$direction;
        }

        return ' ORDER BY '.$keys.' '.$direction;
    }
    /**
     * Create a GROUP BY SQL clause.
     *
     * @param string $group Group By Condition
     *
     * @return mixed string condition or null
     */
    public function group($group)
    {
        if ($group) {
            if (is_array($group)) {
                $group = implode(', ', $group);
            }

            return ' GROUP BY '.$this->__quoteFields($group);
        }

        return;
    }
    /**
     * Disconnects database, kills the connection and says the connection is closed,
     * and if DEBUG is turned on, the log for this object is shown.
     */
    public function close()
    {
        $GLOBALS['_queriesCnt'] = $this->_queriesCnt;
        $GLOBALS['_queriesTime'] = $this->_queriesTime;
        if (Configure::read() > 1) {
            $this->showLog();
        }
        $this->disconnect();
    }
    /**
     * Checks if the specified table contains any record matching specified SQL.
     *
     * @param Model  $model Model to search
     * @param string $sql   SQL WHERE clause (condition only, not the "WHERE" part)
     *
     * @return bool True if the table has a matching record, else false
     */
    public function hasAny(&$Model, $sql)
    {
        $sql = $this->conditions($sql);
        $table = $this->fullTableName($Model);
        $alias = $this->alias.$this->name($Model->alias);
        $where = $sql ? "{$sql}" : ' WHERE 1 = 1';
        $id = $Model->escapeField();

        $out = $this->fetchRow("SELECT COUNT({$id}) {$this->alias}count FROM {$table} {$alias}{$where}");

        if (is_array($out)) {
            return $out[0]['count'];
        }

        return false;
    }
    /**
     * Gets the length of a database-native column description, or null if no length.
     *
     * @param string $real Real database-layer column type (i.e. "varchar(255)")
     *
     * @return mixed An integer or string representing the length of the column
     */
    public function length($real)
    {
        if (!preg_match_all('/([\w\s]+)(?:\((\d+)(?:,(\d+))?\))?(\sunsigned)?(\szerofill)?/', $real, $result)) {
            trigger_error(__('FIXME: Can\'t parse field: '.$real, true), E_USER_WARNING);
            $col = str_replace(array(')', 'unsigned'), '', $real);
            $limit = null;

            if (strpos($col, '(') !== false) {
                list($col, $limit) = explode('(', $col);
            }
            if ($limit != null) {
                return intval($limit);
            }

            return;
        }

        $types = array(
            'int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1,
        );

        list($real, $type, $length, $offset, $sign, $zerofill) = $result;
        $typeArr = $type;
        $type = $type[0];
        $length = $length[0];
        $offset = $offset[0];

        $isFloat = in_array($type, array('dec', 'decimal', 'float', 'numeric', 'double'));
        if ($isFloat && $offset) {
            return $length.','.$offset;
        }

        if (($real[0] == $type) && (count($real) == 1)) {
            return;
        }

        if (isset($types[$type])) {
            $length += $types[$type];
            if (!empty($sign)) {
                --$length;
            }
        } elseif (in_array($type, array('enum', 'set'))) {
            $length = 0;
            foreach ($typeArr as $key => $enumValue) {
                if ($key == 0) {
                    continue;
                }
                $tmpLength = strlen($enumValue);
                if ($tmpLength > $length) {
                    $length = $tmpLength;
                }
            }
        }

        return intval($length);
    }
    /**
     * Translates between PHP boolean values and Database (faked) boolean values.
     *
     * @param mixed $data Value to be translated
     *
     * @return mixed Converted boolean value
     */
    public function boolean($data)
    {
        if ($data === true || $data === false) {
            if ($data === true) {
                return 1;
            }

            return 0;
        } else {
            return !empty($data);
        }
    }
    /**
     * Inserts multiple values into a table.
     *
     * @param string $table
     * @param string $fields
     * @param array  $values
     */
    public function insertMulti($table, $fields, $values)
    {
        $table = $this->fullTableName($table);
        if (is_array($fields)) {
            $fields = implode(', ', array_map(array(&$this, 'name'), $fields));
        }
        $count = count($values);
        for ($x = 0; $x < $count; ++$x) {
            $this->query("INSERT INTO {$table} ({$fields}) VALUES {$values[$x]}");
        }
    }
    /**
     * Returns an array of the indexes in given datasource name.
     *
     * @param string $model Name of model to inspect
     *
     * @return array Fields in table. Keys are column and unique
     */
    public function index($model)
    {
        return false;
    }
    /**
     * Generate a database-native schema for the given Schema object.
     *
     * @param object $schema    An instance of a subclass of CakeSchema
     * @param string $tableName Optional.  If specified only the table name given will be generated.
     *                          Otherwise, all tables defined in the schema are generated.
     *
     * @return string
     */
    public function createSchema($schema, $tableName = null)
    {
        if (!is_a($schema, 'CakeSchema')) {
            trigger_error(__('Invalid schema object', true), E_USER_WARNING);

            return;
        }
        $out = '';

        foreach ($schema->tables as $curTable => $columns) {
            if (!$tableName || $tableName == $curTable) {
                $cols = $colList = $indexes = array();
                $primary = null;
                $table = $this->fullTableName($curTable);

                foreach ($columns as $name => $col) {
                    if (is_string($col)) {
                        $col = array('type' => $col);
                    }
                    if (isset($col['key']) && $col['key'] == 'primary') {
                        $primary = $name;
                    }
                    if ($name !== 'indexes') {
                        $col['name'] = $name;
                        if (!isset($col['type'])) {
                            $col['type'] = 'string';
                        }
                        $cols[] = $this->buildColumn($col);
                    } else {
                        $indexes = array_merge($indexes, $this->buildIndex($col, $table));
                    }
                }
                if (empty($indexes) && !empty($primary)) {
                    $col = array('PRIMARY' => array('column' => $primary, 'unique' => 1));
                    $indexes = array_merge($indexes, $this->buildIndex($col, $table));
                }
                $columns = $cols;
                $out .= $this->renderStatement('schema', compact('table', 'columns', 'indexes'))."\n\n";
            }
        }

        return $out;
    }
    /**
     * Generate a alter syntax from	 CakeSchema::compare().
     *
     * @param unknown_type $schema
     *
     * @return unknown
     */
    public function alterSchema($compare, $table = null)
    {
        return false;
    }
    /**
     * Generate a "drop table" statement for the given Schema object.
     *
     * @param object $schema An instance of a subclass of CakeSchema
     * @param string $table  Optional.  If specified only the table name given will be generated.
     *                       Otherwise, all tables defined in the schema are generated.
     *
     * @return string
     */
    public function dropSchema($schema, $table = null)
    {
        if (!is_a($schema, 'CakeSchema')) {
            trigger_error(__('Invalid schema object', true), E_USER_WARNING);

            return;
        }
        $out = '';

        foreach ($schema->tables as $curTable => $columns) {
            if (!$table || $table == $curTable) {
                $out .= 'DROP TABLE '.$this->fullTableName($curTable).";\n";
            }
        }

        return $out;
    }
    /**
     * Generate a database-native column schema string.
     *
     * @param array $column An array structured like the following: array('name'=>'value', 'type'=>'value'[, options]),
     *                      where options can be 'default', 'length', or 'key'.
     *
     * @return string
     */
    public function buildColumn($column)
    {
        $name = $type = null;
        extract(array_merge(array('null' => true), $column));

        if (empty($name) || empty($type)) {
            trigger_error('Column name or type not defined in schema', E_USER_WARNING);

            return;
        }

        if (!isset($this->columns[$type])) {
            trigger_error("Column type {$type} does not exist", E_USER_WARNING);

            return;
        }

        $real = $this->columns[$type];
        $out = $this->name($name).' '.$real['name'];

        if (isset($real['limit']) || isset($real['length']) || isset($column['limit']) || isset($column['length'])) {
            if (isset($column['length'])) {
                $length = $column['length'];
            } elseif (isset($column['limit'])) {
                $length = $column['limit'];
            } elseif (isset($real['length'])) {
                $length = $real['length'];
            } else {
                $length = $real['limit'];
            }
            $out .= '('.$length.')';
        }

        if (($column['type'] == 'integer' || $column['type'] == 'float') && isset($column['default']) && $column['default'] === '') {
            $column['default'] = null;
        }

        if (isset($column['key']) && $column['key'] == 'primary' && $type == 'integer') {
            $out .= ' '.$this->columns['primary_key']['name'];
        } elseif (isset($column['key']) && $column['key'] == 'primary') {
            $out .= ' NOT NULL';
        } elseif (isset($column['default']) && isset($column['null']) && $column['null'] == false) {
            $out .= ' DEFAULT '.$this->value($column['default'], $type).' NOT NULL';
        } elseif (isset($column['default'])) {
            $out .= ' DEFAULT '.$this->value($column['default'], $type);
        } elseif (isset($column['null']) && $column['null'] == true) {
            $out .= ' DEFAULT NULL';
        } elseif (isset($column['null']) && $column['null'] == false) {
            $out .= ' NOT NULL';
        }

        return $out;
    }
    /**
     * Format indexes for create table.
     *
     * @param array  $indexes
     * @param string $table
     *
     * @return array
     */
    public function buildIndex($indexes, $table = null)
    {
        $join = array();
        foreach ($indexes as $name => $value) {
            $out = '';
            if ($name == 'PRIMARY') {
                $out .= 'PRIMARY ';
                $name = null;
            } else {
                if (!empty($value['unique'])) {
                    $out .= 'UNIQUE ';
                }
            }
            if (is_array($value['column'])) {
                $out .= 'KEY '.$name.' ('.implode(', ', array_map(array(&$this, 'name'), $value['column'])).')';
            } else {
                $out .= 'KEY '.$name.' ('.$this->name($value['column']).')';
            }
            $join[] = $out;
        }

        return $join;
    }
    /**
     * Guesses the data type of an array.
     *
     * @param string $value
     */
    public function introspectType($value)
    {
        if (!is_array($value)) {
            if ($value === true || $value === false) {
                return 'boolean';
            }
            if (is_float($value) && floatval($value) === $value) {
                return 'float';
            }
            if (is_int($value) && intval($value) === $value) {
                return 'integer';
            }
            if (is_string($value) && strlen($value) > 255) {
                return 'text';
            }

            return 'string';
        }

        $isAllFloat = $isAllInt = true;
        $containsFloat = $containsInt = $containsString = false;
        foreach ($value as $key => $valElement) {
            $valElement = trim($valElement);
            if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) {
                $isAllFloat = false;
            } else {
                $containsFloat = true;
                continue;
            }
            if (!is_int($valElement) && !preg_match('/^[\d]+$/', $valElement)) {
                $isAllInt = false;
            } else {
                $containsInt = true;
                continue;
            }
            $containsString = true;
        }

        if ($isAllFloat) {
            return 'float';
        }
        if ($isAllInt) {
            return 'integer';
        }

        if ($containsInt && !$containsString) {
            return 'integer';
        }

        return 'string';
    }
}
